Numerical Hydrodynamics Assignment

Force prediction on three geometries

The assignment is to use numerical methods to generate pressure and friction force predictions for the laminar flow around three geometries:

  1. A circular cylinder,
  2. A jukowski foil with $t/c=0.15$, and
  3. A mystery geometry which will be assigned individually.

Specifically, for the circular cylinder you will:

  • Compute the pressure coefficient $c_p$ on the surface for three resolutions $N=32,64,128$, and compare the results to the exact potential flow solution.
  • Compute the friction drag coefficient $C_F$, and an estimate for the pressure drag coefficient $C_D$ for the same three resolutions at $Re_D=10^5$.

For the jukowski foil you will:

  • Compute the lift coefficient $C_L$ as a function of the angle of attack $\alpha\le 10^o$ for three resolutions $N=32,64,128$, and and compare the results to the exact potential flow solution.
  • Compute the separation point location on either side of the foil for the same angle of attack range for $N=128$.

The mystery geometry will require a similar computation. Correct results for this geometry will demonstrate independent excellence and will be marked accordingly.

1. The circular cylinder:

For the circular cylinder you will:

  • Compute the pressure coefficient $c_p$ on the surface for three resolutions $N=32,64,128$, and compare the results to the exact potential flow solution.
  • Compute the friction drag coefficient $C_F$, and an estimate for the pressure drag coefficient $C_D$ for the same three resolutions at $Re_D=10^5$.

Firstly, I use the numpy to caculate the velocity u and v: $$u(x,y) = \frac{\gamma}{2\pi}\left[\tan^{-1}\left(\frac{x-S}y\right)-\tan^{-1}\left(\frac{x+S}y\right)\right].$$

$$v(x,y) =\frac{\gamma}{4\pi} \log\left(\frac{(x+S)^2+y^2}{(x-S)^2+y^2}\right).$$

It has the code:


In [1]:
import numpy
from matplotlib import pyplot
%matplotlib inline

#Import the required functions from VortexPanel.py and BoundaryLayer.py
from VortexPanel import Panel, solve_gamma, plot_flow, make_circle

In [2]:
pyplot.figure(figsize=(10,6))
def c_p(gamma): return 1-gamma**2
def C_P(theta): return 1-4*(numpy.sin(theta))**2
N_resolution=[32,64,128]
for m in range(3):
    circle = make_circle(N_resolution[m])
    solve_gamma(circle)
    coeff = numpy.zeros(N_resolution[m])
    for i,p_i in enumerate(circle): coeff[i]=c_p(p_i.gamma)
    pyplot.plot(numpy.linspace(1,N_resolution[m],N_resolution[m])/N_resolution[m]*numpy.pi*2,coeff)
    
pyplot.legend(["resolutions(32)","resolutions(64)","resolutions(128)"])
pyplot.title('$C_p$ of the circular cylinder under different resolution')
pyplot.xlabel('phase angle /rad')
pyplot.ylabel('$C_p$')
pyplot.plot(numpy.linspace(0,numpy.pi*2,180),1-4*numpy.sin(numpy.linspace(0,numpy.pi*2,180))**2,lw=2, c='k')


Out[2]:
[<matplotlib.lines.Line2D at 0x9d74a20>]

The black line is the exact potential flow solution, the figure shows when we increase the resolutions which means use more numbers of pannels to model can make the result more accurate.

(2)Compute the friction drag coefficient $C_F$, and an estimate for the pressure drag coefficient $C_D$ for the same three resolutions at $Re_D=10^5$.


In [3]:
def pohlF(eta):
    return 2*eta-2*eta**3+eta**4
def pohlG(eta):
    return eta/6*(1-eta)**3
def pohl(eta,lam):
    return pohlF(eta)+lam*pohlG(eta)
  • $\frac{\delta_1}\delta = \int_0^1 (1-f) d\eta = \frac3{10}-\lambda\frac1{120}$
  • $\frac{\delta_2}\delta = \int_0^1 f(1-f) d\eta = \frac{37}{315}-\lambda\frac1{945}-\lambda^2\frac1{9072}$
  • $\frac 12 c_f Re_\delta =f'(0)= 2+\lambda\frac1{6}$

In [4]:
def disp_ratio(lam): return 3./10.-lam/120.
def mom_ratio(lam): return 37./315.-lam/945.-lam**2/9072.
def df_0(lam): return 2+lam/6.
$$ g_1(\lambda) = \frac 12 c_f Re_\delta - \lambda \left[\frac{\delta_1}{\delta}+2\frac{\delta_2}\delta\right]$$

In [5]:
def g_1(lam): return df_0(lam)-lam*(disp_ratio(lam)+2*mom_ratio(lam))

In [6]:
from scipy.optimize import bisect
lam0 = bisect(g_1,-12,12)         # use bisect method to find root between -12...12
print 'lambda_0 = ',lam0


lambda_0 =  7.05232310118

In [7]:
def ddx_delta(Re_d,lam):
    if Re_d==0: return 0                     # Stagnation point condition
    return g_1(lam)/mom_ratio(lam)/Re_d      # delta'

In [8]:
def heun(g,psi_i,i,dx,*args):
    g_i = g(psi_i,i,*args)                      # integrand at i
    tilde_psi = psi_i+g_i*dx                    # predicted estimate at i+1
    g_i_1 = g(tilde_psi,i+1,*args)              # integrand at i+1
    return psi_i+0.5*(g_i+g_i_1)*dx             # corrected estimate

In [9]:
def g_pohl(delta_i,i,u_e,du_e,nu):
    Re_d = delta_i*u_e[i]/nu            # compute local Reynolds number
    lam = delta_i**2*du_e[i]/nu         # compute local lambda 
    return ddx_delta(Re_d,lam)          # get derivative

In [10]:
def march(x,u_e,du_e,nu):
    delta0 = numpy.sqrt(lam0*nu/du_e[0])                # set delta0
    delta = numpy.full_like(x,delta0)                   # delta array
    lam = numpy.full_like(x,lam0)                       # lambda array
    for i in range(len(x)-1):                           # march!
        delta[i+1] = heun(g_pohl,delta[i],i,x[i+1]-x[i],    # integrate BL using...
                          u_e,du_e,nu)                          # additional arguments
        lam[i+1] = delta[i+1]**2*du_e[i+1]/nu               # compute lambda
        if abs(lam[i+1])>12: break                          # check stop condition
    return delta,lam,i                                  # return with separation index

In [11]:
nu=1e-5                                     # viscosity
N = 32                                      # number of steps
s = numpy.linspace(0,numpy.pi,N)            # distance goes from 0..pi
u_e = 2.*numpy.sin(s)                       # velocity
du_e = 2.*numpy.cos(s)                      # gradient
delta,lam,iSep = march(s,u_e,du_e,nu)       # solve!

In [12]:
def half_c_f(lam, nu, delta, u_e):
    Re_d = delta*u_e/nu
    return df_0(lam)/Re_d

In [13]:
def taw_w(lam, nu, delta, u_e):
    if u_e == 0: 
        return 0
    else:
        return half_c_f(lam, nu, delta, u_e)*u_e**2

In [14]:
def C_F_calc(tau, sx, N):
    #return numpy.sum(tau[:iSep+1]*sx*numpy.pi/(N-1))
    return numpy.trapz(tau[:iSep+1]*sx, dx = numpy.pi/(N-1))

In [15]:
for N in N_resolution:
    s = numpy.linspace(0,numpy.pi,N)            # distance goes from 0..pi
    u_e = 2.*numpy.sin(s)                       # velocity
    du_e = 2.*numpy.cos(s)                      # gradient
    delta, lam, iSep = march(s,u_e,du_e,nu)     # solve!
    taw = numpy.full_like(delta, 0)
    taw = [taw_w(lam[i], nu, delta[i], u_e[i]) for i in range(N)]
    sx = numpy.sin(s[0:iSep+1])
    print ('When N = ' + '%i' %N)
    C_F_circle = 2*C_F_calc(taw, sx, N)/numpy.pi
    print ('Circle frictional coefficient = ' + '%0.2e' %C_F_circle)
    C_F_flat = 1.33 * numpy.sqrt(nu/numpy.pi)
    print("Flate plate: "+'%0.2e' %C_F_flat)


When N = 32
Circle frictional coefficient = 4.04e-03
Flate plate: 2.37e-03
When N = 64
Circle frictional coefficient = 4.06e-03
Flate plate: 2.37e-03
When N = 128
Circle frictional coefficient = 4.07e-03
Flate plate: 2.37e-03

By using the code numpy.trapz to integrate and get the drag coefficient in three resolutions are:

Resolution Circle Flat plat
32 4.04e-03 2.37e-3
64 4.06e-03 2.37e-3
128 4.07e-03 2.37e-3

In [16]:
s_x = numpy.sin(s)
pyplot.figure(figsize=(8,6))
pyplot.plot(s,taw*s_x)
pyplot.scatter(s[iSep+1], taw[iSep+1]*s_x[iSep+1],s=50,c="r")
pyplot.xlabel('$s$',size=20)
pyplot.ylabel(r'$\tau_w s_x$', size=20)


Out[16]:
<matplotlib.text.Text at 0xb2c7208>

Estimate for the pressure drag coefficient CD


In [17]:
def cylinder_separation(s,iSep):
    c1=C_P(s[0:iSep+1])
    c2=C_P(s[iSep])*numpy.ones(s.size-iSep-1)
    return numpy.concatenate((c1,c2),axis=0)

In [18]:
N=64
s = numpy.linspace(0,numpy.pi,N)            # distance goes from 0..pi
u_e = 2.*numpy.sin(s)                       # velocity
du_e = 2.*numpy.cos(s)                      # gradient
delta, lam, iSep = march(s,u_e,du_e,nu)     # solve!
C_P_s= cylinder_separation(s,iSep)

In [19]:
pyplot.figure(figsize=(10,6))
pyplot.plot(s,cylinder_separation(s,iSep))
pyplot.xlabel('AoA alpha (rad)',size=20)
pyplot.ylabel('$Pressure coefficient C_p$', size=20)
pyplot.scatter(s[iSep+1],C_P_s[iSep+1],s=50,c="r")
pyplot.legend(["$C_p$"])


Out[19]:
<matplotlib.legend.Legend at 0xb419860>

In [20]:
def coefficient_Cd(N,sy,C_P_s):
    return numpy.trapz(C_P_s*sy, dx=numpy.pi/(N-1))

In [21]:
for N in N_resolution:
    s = numpy.linspace(0,numpy.pi,N)            # distance goes from 0..pi
    s_y=numpy.cos(s)
    u_e = 2.*numpy.sin(s)                       # velocity
    du_e = 2.*numpy.cos(s)                      # gradient
    delta, lam, iSep = march(s,u_e,du_e,nu)     # solve!
    C_P_s=cylinder_separation(s,iSep)
    print ("The drag coefficient is:")
    print coefficient_Cd(N,s_y,C_P_s)


The drag coefficient is:
2.41893548508
The drag coefficient is:
2.37858006043
The drag coefficient is:
2.35785834059

2. A jukowski foil with $t/c=0.15$

For the jukowski foil you will:

  • Compute the lift coefficient $C_L$ as a function of the angle of attack $\alpha\le 10^o$ for three resolutions $N=32,64,128$, and and compare the results to the exact potential flow solution.
  • Compute the separation point location on either side of the foil for the same angle of attack range for $N=128$.

In [22]:
from VortexPanel import Panel,solve_gamma_kutta,plot_flow,make_jukowski,make_circle
N = 64
foil = make_jukowski(N)

In [23]:
#calculate C_L in a range of angle of attack
def C_L_AA(foil,alpha_max,M):
    C_L=numpy.zeros(M)
    for i in range(M):
        alpha=alpha_max*i*numpy.pi/180./(M-1)
        solve_gamma_kutta(foil,alpha)       # solve for gamma
        C_L[i]=lift(foil)            # print the lift
    return C_L

In [24]:
def CLA(rr,alpha): 
    return 2.*numpy.pi*(1+4./numpy.sqrt(3)/3.*rr)*numpy.sin(alpha)

In [25]:
def lift(panels):
    c = panels[0].x[0]-panels[len(panels)/2].x[0]      # length scale
    return -4./c*numpy.sum([p.gamma*p.S for p in panels])

In [26]:
pyplot.figure(figsize=(10,6))
N=[32,64,128]
M=101
alpha_max=10.
for p in range(3):
    Na=N[p]
    foil1 = make_jukowski(Na,dx=0.15,dy=0,dr=0)             # make foil
    coff1=C_L_AA(foil1,10,M)
    pyplot.plot(numpy.linspace(1,M,M)/M*alpha_max,coff1)
    
M=101
rr=0.15
coff2=numpy.zeros(M)
for q in range(M):
    alpha=alpha_max*q*numpy.pi/180./(M-1)
    coff2[q]=CLA(rr,alpha)
pyplot.plot(numpy.linspace(1,M,M)/M*alpha_max,coff2)

pyplot.xlabel('Angle of attack /deg')
pyplot.ylabel('$C_L$')
pyplot.legend(["N=32","N=64","N=128","Analytical"])


Out[26]:
<matplotlib.legend.Legend at 0xb7d0eb8>

As we ca see in the picture: when the resolution gets higher, the lift coefficient which we calculate is closer to the analytical value.

(2) Compute the separation point:


In [27]:
from BoundaryLayer import Pohlhausen, march 
def solve_plot_boundary_layers(panels,alpha=0,nu=1e-5):

    # split the panels
    top_panels,bottom_panels = split_panels(panels)
    
    # Set up and solve the top boundary layer
    top = Pohlhausen(top_panels,nu)
    top.march()

    # Set up and solve the bottom boundary layer
    bottom = Pohlhausen(bottom_panels,nu)
    bottom.march()

    return top,bottom

In [28]:
def predict_jukowski_separation(t_c,alpha=0,N=128):
    #Function from separation prediction and altered to return a value.
    # set dx to gets the correct t/c
    foil = make_jukowski(N,dx=t_c-0.019) #t_c-0.019 is the shift from t/c to dx

    # find and print t/c
    x0 = foil[N/2].xc
    c = foil[0].xc-x0
    t = 2.*numpy.max([p.yc for p in foil])
    #print "t/c = "+"%.3f"%(t/c)

    # solve potential flow and boundary layer evolution
    solve_gamma_kutta(foil,alpha)
    top,bottom = solve_plot_boundary_layers(foil,alpha)

    #Return point of seperation
    return (top.x_sep-x0)/c, (bottom.x_sep-x0)/c
degrees = numpy.linspace(0, 10, 11) #For the graph axis

In [29]:
alpha = numpy.linspace(0, 10./180*numpy.pi, 11)

In [30]:
def split_panels(panels):
    # positive velocity defines `top` BL
    top = [p for p in panels if p.gamma<=0]      
    # negative defines the `bottom`
    bottom = [p for p in panels if p.gamma>=0]
    # reverse array so panel[0] is stagnation
    bottom = bottom[::-1]

    return top,bottom

In [31]:
sep_point = [] #Create empty list as it's easy.
for a in alpha:
    sep_point.append(predict_jukowski_separation(0.15, a))
sep_point = numpy.array(sep_point) # Turn sep_point from list into an array

In [32]:
pyplot.figure(figsize=(10,7))
pyplot.ylabel(r'$\frac{x}{c}$', fontsize=24)
pyplot.xlabel(r'Angle of Attack $^o$', fontsize=16)
pyplot.plot(degrees, sep_point[:,0], label=r'Top Separation Point')
pyplot.plot(degrees, sep_point[:,1], label=r'Bottom Separation Point')
pyplot.legend(loc='right')
pyplot.show()


As we can see in the picture, when the angle of attack increases the top separation point move to the leading edge, however, the bottom separation point move to the trailing edge.

A mystery geometry

Cambered Jukowski foil

The geometry is the 15% Jukowski foil with camber, quantified by the height of the mean line at $x=0$, ie $\bar y = \frac 12 (y[N/4]+y[3N/4])$.


Assignment

Compute the lift force coefficient $C_L$ and boundary layer separation point locations for $\bar y/c = 0.02,0.04,0.08$ at $\alpha=0$ using $N=128$ panels, and compare to the symmetric foil solution.


In [33]:
def camber(t_c, alpha, dy, N=128):
    #Function to find empirical relationship between dy and camber.
    dx=t_c-0.019
    foil = make_jukowski(N, dx, dy)
    #plot_flow(foil, alpha) #Can be commented in to inspect the shape of the foil
    a = int(N/4)
    b = int((3*N)/4)
    y_bar = 0.5*(foil[a].yc + foil[b].yc)
    return y_bar

trial_dy = numpy.linspace(0, -1, 30) #Range of experimental values for dy
y_bar = [] #Create empty list.
for i in trial_dy:
    y_bar.append(camber(0.15, 0, i, 128))

In [34]:
#Investigating the relationship between dy and y_bar
def camber_emp(trial_dy, y_bar):
    from sklearn.linear_model import LinearRegression 
    #This is a module developed for machine learning. It is useful for this sort of analysis.
    X = trial_dy[:, numpy.newaxis] 
    regr = LinearRegression() 
    regr.fit(X, y_bar)
    fig = pyplot.figure(figsize=(8,6))
    ax = fig.add_subplot(1,1,1)   
    pyplot.ylabel(r"$\bar y$ ",fontsize=16)
    pyplot.xlabel("dy ",fontsize=16)
    pyplot.plot(trial_dy, y_bar)
    pyplot.gca().set_xlim(left=-1)
    pyplot.gca().set_ylim(bottom=0) 
    print("Multiply the dy value by %.5f to obtain the ybar/c value for the foil geometry chosen." % regr.coef_)
    pyplot.show()
    return regr.coef_

coef = camber_emp(trial_dy, y_bar)


Multiply the dy value by -0.84675 to obtain the ybar/c value for the foil geometry chosen.

we can see the realtionship between dy and y bar is linear correlation.


In [35]:
def jukowski_CL(alpha,t_c=0.15+0.019): return 2.*numpy.pi*(1+4/3/numpy.sqrt(3)*t_c)*numpy.sin(alpha)
def ybar_c(y_bar,c):
    ybar_c=y_bar/c
    return ybar_c
def predict_jukowski_separation_camber(t_c, ybar_c, alpha=0,N=128):
    #Function from separation prediction and altered to account for camber.
    # set dx to gets the correct t/c
    dx = t_c  -0.019
    dy = ybar_c/(-0.84675)
    foil = make_jukowski(N, dx, dy) #t_c-0.019 is the shift from t/c to dx
    x0 = foil[N/2].xc
    c = foil[0].xc-x0
    t = 2.*numpy.max([p.yc for p in foil])

    # solve potential flow and boundary layer evolution
    solve_gamma_kutta(foil,alpha)
    top,bottom = solve_plot_boundary_layers(foil,alpha)

    #Return point of separation
    return (top.x_sep-x0)/c, (bottom.x_sep-x0)/c

def calc_CL_camb(alpha, N, t_c, ybar_c):
    #Function from earlier in this report altered to account for camber.
    dx = t_c - 0.019
    dy = ybar_c/(-0.84675)
    foil = make_jukowski(N, dx, dy)
    solve_gamma_kutta(foil, alpha)
    return lift(foil) 

fill_val = jukowski_CL(0) #The lift coefficient for the symmetric foil.
analytic = numpy.full_like(ybar_c, fill_val) #Python will get sad unless it gets an array of analytical C_L to plot.
analytic


Out[35]:
array(0.0, dtype=object)

In [36]:
ybar_c = [0.02, 0.04, 0.08] #List of cambers.
c_l = []
for i in ybar_c:
    c_l.append(calc_CL_camb(0, 128, 0.15, i))
sep_points_camber = []
for i in ybar_c:
    sep_points_camber.append(predict_jukowski_separation_camber(0.15, i, 0, 128))
sep_points_camber = numpy.array(sep_points_camber)

In [ ]:


In [ ]:


In [ ]:

Conclusion

Well,I found that if I use higher resolution(by using more numbers of pannels to model) the result I got was closeer to the analytical values. However it will cost more time to calculate.